Skip to content

Conversation

@rozyczko
Copy link
Member

As requested and described by @jfkcooper this is the library implementation of bilayers in EasyReflectometryLib.

Please scrutinize docs\tutorials\simulation\bilayer.ipynb for correctness and API.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request does not contain a valid label. Please add one of the following labels: ['chore', 'fix', 'bugfix', 'bug', 'enhancement', 'feature', 'dependencies', 'documentation']

@rozyczko rozyczko requested a review from jfkcooper January 31, 2026 14:02
@rozyczko rozyczko added [scope] enhancement Adds/improves features (major.MINOR.patch) [priority] high Should be prioritized soon labels Jan 31, 2026
@rozyczko rozyczko mentioned this pull request Jan 31, 2026
@codecov
Copy link

codecov bot commented Jan 31, 2026

Codecov Report

❌ Patch coverage is 91.26984% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.29%. Comparing base (205db70) to head (4333c43).

Files with missing lines Patch % Lines
src/easyreflectometry/sample/assemblies/bilayer.py 91.20% 0 Missing and 11 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                @@
##           append_sample     #292      +/-   ##
=================================================
+ Coverage          88.10%   88.29%   +0.19%     
=================================================
  Files                 45       46       +1     
  Lines               2471     2597     +126     
  Branches             288      307      +19     
=================================================
+ Hits                2177     2293     +116     
+ Misses               233      232       -1     
- Partials              61       72      +11     
Flag Coverage Δ
unittests 88.29% <91.26%> (+0.19%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/easyreflectometry/sample/__init__.py 100.00% <100.00%> (ø)
src/easyreflectometry/sample/assemblies/bilayer.py 91.20% <91.20%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Collaborator

@jfkcooper jfkcooper left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically only one comment on the code parts, and easily rectified. I will run the notebook now and give you feedback on this, but doing it in github is not fun for notebooks, so I submit these comments now

@jfkcooper
Copy link
Collaborator

Notes from the notebook:

  • Depending on what the aim for the notebook is, I would remove all of the cells doing checking an printing, as they would not be interesting to a user - it would be assumed that they just work.

  • The superphase for the complete sample should be silicon. The structure should be:
    Si | SiO2 | head | tail | tail | head | water it should look something like this:

image
  • For the tail groups, I believe you have the SLD for a deuterated version of the tails, but this is not the 'normal' configuration, and I think for an exemplar notebook it is better to use the hydrogenous version.

  • For the asymmetric hydration, it isn't clear where but there seems to be a bug or something, as you would expect a difference in the curves. I would highlight the change by showing the SLD curves rather than just the reflectivity though, as this is where you can know that it is being done correctly

@jfkcooper
Copy link
Collaborator

Thinking on it, I would also use this as a way to demonstrate mutliple contrasts for the lipid model, so using H2O as the solvent and subphase as well as D2O, and then showing the SLD curves, and the reflectivity curves (and the ability to set the constraints between the two models (or however you would put the models together).

The most common use case of the lipid bilayer would be for fitting mulitple contrast (water) data.

@rozyczko
Copy link
Member Author

rozyczko commented Feb 2, 2026

Addressed the code issues and modified the notebook to reflect Jos' comments.

@rozyczko rozyczko requested a review from jfkcooper February 2, 2026 14:17
@jfkcooper
Copy link
Collaborator

Thanks for the changes, though I just noticed a couple more things:

  • In cell 3 you solvate the head and the tail with different solvents. These should strictly always be the same solvent
  • The inner and outer headgroups are hydrated differently - but it isn't clear how this happened, or how one would set them separately ('front_head_layer' and 'back_head_layer' are not mentioned before cell 7
  • Cell 8 has the same parameter being printed as cell 6, so, not really demonstrating that the roughness is conformal.
  • I think cell 13 has some bug as the two curves should be different, but aren't
  • Same bug with cell 14
  • cell 15 - the head and tails should be solvated with the same liquid. It is also a little confusing that the tails are now a different lipid (hydrogenous rather than deuterated tail group), I would keep it consistent throughout the notebook
  • The above point is especially important in cells 18 and 19, since the two curves are not presenting the same lipid system.

Let me know if you want a chat about any of these points, and thanks again for the progress!

Copy link
Member

@henrikjacobsenfys henrikjacobsenfys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments and questions


def __init__(
self,
front_head_layer: Optional[LayerAreaPerMolecule] = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move towards | typing in new files? Or keep things consistent within a project?

layer is created internally with parameters constrained to this one.
:param back_head_layer: Layer representing the back head part of the bilayer.
:param name: Name for bilayer, defaults to 'EasyBilayer'.
:param constrain_heads: Constrain head layer thickness and area per molecule, defaults to `True`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constrain in what way?

unique_name = global_object.generate_unique_name(self.__class__.__name__)

# Create default front head layer if not provided
if front_head_layer is None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to put these default values or the constructor in a defaults file and generate them from there, passing onlythe unique_name? To make them easier to maintain across files

unique_name: Optional[str] = None,
constrain_heads: bool = True,
conformal_roughness: bool = True,
interface=None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should interface have a type?

interface=interface,
)

# Store the front tail layer reference for constraint setup
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and back tail

self.conformal_roughness = True

def _setup_tail_constraints(self) -> None:
"""Setup constraints so back tail layer parameters depend on front tail layer."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are identical to? At least from what I can tell.

self.layers[0] = layer

@property
def front_tail_layer(self) -> Optional[LayerAreaPerMolecule]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I right that self.layers[1] is identical to self._front_tail_layer? This seems like it may be problematic if they get out of sync? Not sure how it works, just wanted to point it out

if unique_name is None:
unique_name = global_object.generate_unique_name(self.__class__.__name__)

# Create default front head layer if not provided
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this should be its own little method? _create_default_front_head_layer to make the init a bit shorter? (Also applies elsewhere)

)

# Constrain area per molecule
back_tail._area_per_molecule.make_dependent_on(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to use a public method instead? Also elsewhere.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[priority] high Should be prioritized soon [scope] enhancement Adds/improves features (major.MINOR.patch)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants